feat(backup): automatic iCloud Drive backup with restore after app deletion - #15
Merged
Conversation
…letion Notes now mirror to the user's own iCloud Drive container so deleting the app no longer destroys them. No account, no server, no sync UI - disk stays the source of truth and the mirror follows it, in keeping with local-first. - Native ICloudBackupModule (Swift): ubiquity container resolution, evicted-file download, and generic file ops - expo-file-system cannot reach paths outside the app sandbox, which includes the container. - Pure backup engine (backupEngine.ts) with injected env, node-tested: debounced mirror sync (changed files only, one-call native directory listing), manifest with size/mtime diffing, and restore into an empty library with per-path safety validation restricted to the data dirs. - Deletions propagate ONLY via catalog tombstones (deletedNoteIds, pruned after 90 days). A file merely missing locally - fresh install, partial restore, older device - never deletes its backup copy, and an empty local library never overwrites a backup that has notes. This guard exists because simulator E2E caught the reinstall race where a startup sync wiped the backup while the restore prompt was on screen. - Restore-on-reinstall: when the library is empty and the backup has notes, the library offers a one-tap restore; catalog reconciliation recovers anything the mirrored catalog missed. - Default on, with an off switch (confirmed, destructive-styled) and an honest status line in the support sheet; hidden on Android until a backup implementation exists there. iCloud signed-out degrades to a visible 'unavailable' state and self-heals when it appears. - Dev-only container override enables full E2E: verified on iPad simulator - first mirror, change propagation, delete-app/reinstall/ restore, tombstoned delete, toggle off/on with catch-up, corrupt manifest self-heal, unavailable, and the backup-wipe regression. Claude-Session: https://claude.ai/code/session_01Q73aeGnnUsUZ7BLLXJq2u5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
A customer permanently lost their notes by deleting the app — the durability work in #14 cannot survive that, because deletion wipes the Documents directory. Notes now mirror automatically into the user's own iCloud Drive container, which survives app deletion, so a reinstall offers a one-tap restore. No account, no server, no sync UI: disk stays the source of truth (local-first), the mirror follows it.
Design
ICloudBackupModule, Swift): ubiquity container resolution, evicted-file download, and generic file ops. Required because expo-file-system refuses paths outside the app sandbox scopes — which includes the iCloud container (a real bug the simulator E2E caught before it shipped).backupEngine.ts, env-injected, node-tested): debounced (8s) mirror sync copying only changed files (size/mtime manifest diff, single-call native directory listing), and restore into an empty library only, with every backup-derived path validated (isSafeRelPath+ restricted to the three data dirs).deletedNoteIdsin the catalog, pruned after 90 days). A file merely missing locally — fresh install, partial restore — never deletes its backup copy, and an empty local library never overwrites a backup that has notes. This design exists because E2E caught a catastrophic race: the startup sync on a fresh install wiped the backup while the restore prompt was still on screen. That scenario is now a regression test (DISASTER REPRO) and was re-verified live.Verification
simctl uninstall: first mirror, change propagation, delete app → reinstall → restore prompt → 2 files restored → notes open, tombstoned delete removes only that note from backup, toggle off (confirm) / on (instant catch-up), corrupt manifest self-heals, iCloud-unavailable degrades visibly, and the backup-wipe race regression (30s in prompt state, backup intact)Before releasing this (not part of the PR)
iCloud.com.builderpro.opennotesmust exist on the App ID in the developer portal — the first archive with-allowProvisioningUpdatesshould register it, but verify before submitting.https://claude.ai/code/session_01Q73aeGnnUsUZ7BLLXJq2u5